import AS from 'assert';
import { resolve } from 'path';

import { dirCacheManifest } from '../../../../lib/global.js';
import { toHexL, toSize, unZstd } from '../lib/Tool.js';

import Biffer from '../lib/Biffer.js';

import Bundle from './Bundle.js';
import Lang from './Lang.js';
import FileEntry from './FileEntry.js';
import Directory from './Directory.js';
import File from './File.js';
import { parseTable } from './Table.js';

export default class Manifest {
	constructor(url, version, buffer) {
		this.url = url;
		this.version = version;
		this.buffer = buffer;
	}

	async parseRman(wog) {
		try {
			wog.add(wog.wait, '解析[索引头]', '', {
				1: { stat: wog.ready, title: 'BODY大小', text: '' },
			});

			const { buffer } = this;
			const bifferManifest = new Biffer(buffer);

			const [codeMagic, versionMajor, versionMinor] = bifferManifest.unpack('<4sBB');

			AS(codeMagic == 'RMAN', `魔术字(${codeMagic})不为RMAN`);
			AS(versionMajor == 2 && versionMinor == 0, `版本(${versionMajor}.${versionMinor})不支持`);

			// eslint-disable-next-line no-unused-vars
			const [bitsFlag, offset, length, idManifest, lengthBody] = bifferManifest.unpack('<HLLQL');

			AS(bitsFlag & (1 << 9), `标记位(${bitsFlag})不为512`);
			AS(offset == bifferManifest.tell(), `偏移量(${offset})与数据长度(${bifferManifest.tell()})不一致`);

			this.id = idManifest;

			this.buffer = await unZstd(resolve(dirCacheManifest, `${this.version}-${toHexL(this.id, 0, false)}-body.manifest`), bifferManifest.raw(length), true);

			wog.set(
				0, wog.done, '完成',
				1, wog.done, toSize(this.buffer.length)
			);
		}
		catch(error) {
			wog.set(0, wog.error, error?.message ?? error?.toString() ?? `[${typeof error}] ${error}`);

			throw error;
		}
	}

	async parseBody(wog) {
		try {
			wog.add(wog.wait, '解析[索引]', '', {
				1: { stat: wog.ready, title: '解析子包', text: '' },
				2: { stat: wog.ready, title: '解析语言', text: '' },
				3: { stat: wog.ready, title: '解析文件', text: '' },
				4: { stat: wog.ready, title: '解析目录', text: '' },
				5: { stat: wog.ready, title: '构建索引', text: '' },
			});

			const biffer = new Biffer(this.buffer);

			// header (unknown values, skip it)
			const [n] = biffer.unpack('<l');
			biffer.skip(n);

			// offsets to tables(convert to absolute)
			const offsetsBase = biffer.tell();
			const d = biffer.unpack('<6l');
			const offsets = d.map((v, i) => offsetsBase + 4 * i + v);

			biffer.seek(offsets[0]);
			this.bundles = await parseTable(biffer, Bundle, wog, 1);

			biffer.seek(offsets[1]);

			this.langs = {};
			(await parseTable(biffer, Lang, wog, 2)).forEach(lang => this.langs[lang.langID] = lang.lang);

			// Build a map of chunks, indexed by ID
			// Some of ChunkIDs are duplicates, but they are always the same size
			this.chunks = {};
			for(const bundle of this.bundles) {
				for(const chunk of bundle.chunks) {
					AS(!this.chunks[chunk.id] || (this.chunks[chunk.id].size == chunk.size || this.chunks[chunk.id].targetSize == chunk.targetSize));

					this.chunks[chunk.id] = chunk;

					chunk.idBundle = bundle.id;
				}
			}

			biffer.seek(offsets[2]);

			this.fileEntries = await parseTable(biffer, FileEntry, wog, 3);

			biffer.seek(offsets[3]);

			const directories = await parseTable(biffer, Directory, wog, 4);
			const directories_id = {};
			for(const directory of directories) {
				directories_id[directory.id] = directory;
			}


			// merge files and directory data
			wog.set(5, wog.wait, '');
			const files = {};
			for(const fileEntry of this.fileEntries) {
				const { id, link, langIDs, sizeFile, idsChunk } = fileEntry;
				let { name, idDirectory } = fileEntry;

				while(idDirectory) {
					const directory = directories_id[idDirectory] || {};

					const dirName = directory.name;
					idDirectory = directory.parentID;

					name = `${dirName}/${name}`;
				}

				const langs = (langIDs || []).map(id => this.langs[id]);
				const fileChunks = (idsChunk || []).map(id => this.chunks[id]);

				files[name] = new File(id, name, sizeFile, link, langs, fileChunks, this.version);
			}

			this.files = files;

			wog.set(0, wog.done, '完成', 5, wog.done, '完成');
		}
		catch(error) {
			wog.set(0, wog.error, error?.message ?? error?.toString() ?? `[${typeof error}] ${error}`);

			throw error;
		}
	}
}